use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use crate::core::operation::Operation;
use crate::core::services::job::job::Error::{CreateWorkspace, RunStep};
use crate::core::services::job::job_context::{JobContext, Workspace};
use crate::core::state_time::StateTime;
use crate::core::step::Step;
use chrono::{DateTime, Utc};
use grpc_server::State::Paused;
use grpc_server::{Info, OkType, State};
use snafu::{ResultExt, Snafu};
use tempdir::TempDir;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum Error {
    #[snafu(display("Failed to create workspace of job: {workspace}"))]
    CreateWorkspace { workspace: String },

    #[snafu(display("Failed to run step with {step_id}"))]
    RunStep { step_id: usize },
}

pub struct Job {
    id: usize,
    workspace: Option<PathBuf>,
    create_time: DateTime<Utc>,
    start_time: Mutex<DateTime<Utc>>,
    steps: Vec<Step>,
    state_time: Mutex<StateTime>,
    result: Mutex<Option<Result<OkType, Error>>>,

    context: Arc<Mutex<Option<Arc<JobContext>>>>,
}

#[derive(Debug)]
pub enum JobCommand {
    Pause,
    Stop,
}

pub struct JobBuilder {
    id: usize,
    workspace: Option<PathBuf>,
    operations: Vec<Box<dyn Operation>>,
}

impl Job {
    pub fn id(&self) -> usize {
        self.id
    }

    pub fn create_timestamp(&self) -> DateTime<Utc> {
        self.create_time
    }

    pub fn start_time(&self) -> DateTime<Utc> {
        let start_time = self.start_time.lock().unwrap();
        *start_time
    }

    pub fn steps(&self) -> impl Iterator<Item = &Step> {
        self.steps.iter()
    }

    pub fn state(&self) -> State {
        let state_time = self.state_time.lock().unwrap();

        (*state_time).state()
    }

    pub fn state_time(&self) -> DateTime<Utc> {
        let state_time = self.state_time.lock().unwrap();

        (*state_time).time()
    }

    pub fn context(&self) -> Arc<JobContext> {
        let guard = self.context.lock().unwrap();
        match &*guard {
            None => {
                panic!("The context of job does not exists")
            }
            Some(j) => j.clone(),
        }
    }

    pub fn info(&self) -> Info {
        Info {
            id: self.id.try_into().unwrap(),
            create_timestamp: self.create_time.timestamp(),
            start_timestamp: self.start_time().timestamp(),
            state: self.state().into(),
            state_timestamp: self.state_time().timestamp(),
        }
    }

    async fn run_internal(&self) -> Result<OkType, Error> {
        let workspace = match &self.workspace {
            None => {
                let dir_name = self.id.to_string();
                let tmp_dir = TempDir::new(&dir_name).map_err(|_| CreateWorkspace {
                    workspace: dir_name,
                })?;
                Workspace::TempDir(tmp_dir)
            }
            Some(workspace) => Workspace::PathBuf(workspace.clone()),
        };
        *self.context.lock().unwrap() = Some(Arc::new(JobContext::new(workspace)));

        *self.start_time.lock().unwrap() = Utc::now();
        self.change_state(State::Running);

        for step in self.steps.iter() {
            if let Ok(command) = self.context().try_get_command() {
                match command {
                    JobCommand::Pause => {
                        self.change_state(Paused);
                    }
                    JobCommand::Stop => {
                        self.change_state(State::Stopped);
                    }
                };

                return Ok(OkType::Interrupted);
            }

            if !step.run(self.context()).await {
                self.change_state(State::Stopped);
                return Err(RunStep { step_id: step.id() });
            }
        }

        self.change_state(State::Stopped);
        Ok(OkType::Completed)
    }

    pub async fn run(&self) {
        self.set_result(self.run_internal().await);
    }

    fn change_state(&self, s: State) {
        *self.state_time.lock().unwrap() = StateTime::new(s);
        self.context().send_change();
    }

    fn set_result(&self, result: Result<OkType, Error>) {
        *self.result.lock().unwrap() = Some(result);
        *self.context.lock().unwrap() = None;
    }
}

impl JobBuilder {
    pub fn new(id: usize) -> Self {
        JobBuilder {
            id,
            workspace: None,
            operations: vec![],
        }
    }

    pub fn set_workspace(mut self, path: PathBuf) -> Self {
        let path_buf = PathBuf::from(path);
        self.workspace = Some(path_buf);

        self
    }

    pub fn add_step(mut self, operation: impl Operation + Send + Sync + 'static) -> Self {
        self.operations.push(Box::new(operation));
        self
    }

    pub fn build(self) -> Job {
        let steps = self
            .operations
            .into_iter()
            .enumerate()
            .map(|(i, opt)| Step::new(i, opt))
            .collect();

        Job {
            id: self.id,
            workspace: self.workspace,
            create_time: Utc::now(),
            start_time: Mutex::new(Default::default()),
            steps,
            state_time: Mutex::new(StateTime::new(State::Created)),
            result: Mutex::new(None),

            context: Arc::new(Mutex::new(None)),
        }
    }
}
